package com.thaiopensource.relaxng.output.rnc;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import com.thaiopensource.relaxng.edit.Annotated;
import com.thaiopensource.relaxng.edit.AnnotationChild;
import com.thaiopensource.relaxng.edit.AnnotationChildVisitor;
import com.thaiopensource.relaxng.edit.AnyNameNameClass;
import com.thaiopensource.relaxng.edit.AttributeAnnotation;
import com.thaiopensource.relaxng.edit.AttributePattern;
import com.thaiopensource.relaxng.edit.ChoiceNameClass;
import com.thaiopensource.relaxng.edit.ChoicePattern;
import com.thaiopensource.relaxng.edit.Combine;
import com.thaiopensource.relaxng.edit.Comment;
import com.thaiopensource.relaxng.edit.Component;
import com.thaiopensource.relaxng.edit.ComponentVisitor;
import com.thaiopensource.relaxng.edit.CompositePattern;
import com.thaiopensource.relaxng.edit.Container;
import com.thaiopensource.relaxng.edit.DataPattern;
import com.thaiopensource.relaxng.edit.DefineComponent;
import com.thaiopensource.relaxng.edit.DivComponent;
import com.thaiopensource.relaxng.edit.ElementAnnotation;
import com.thaiopensource.relaxng.edit.ElementPattern;
import com.thaiopensource.relaxng.edit.EmptyPattern;
import com.thaiopensource.relaxng.edit.ExternalRefPattern;
import com.thaiopensource.relaxng.edit.GrammarPattern;
import com.thaiopensource.relaxng.edit.GroupPattern;
import com.thaiopensource.relaxng.edit.IncludeComponent;
import com.thaiopensource.relaxng.edit.InterleavePattern;
import com.thaiopensource.relaxng.edit.ListPattern;
import com.thaiopensource.relaxng.edit.MixedPattern;
import com.thaiopensource.relaxng.edit.NameClass;
import com.thaiopensource.relaxng.edit.NameClassVisitor;
import com.thaiopensource.relaxng.edit.NameClassedPattern;
import com.thaiopensource.relaxng.edit.NameNameClass;
import com.thaiopensource.relaxng.edit.NamespaceContext;
import com.thaiopensource.relaxng.edit.NotAllowedPattern;
import com.thaiopensource.relaxng.edit.NsNameNameClass;
import com.thaiopensource.relaxng.edit.OneOrMorePattern;
import com.thaiopensource.relaxng.edit.OptionalPattern;
import com.thaiopensource.relaxng.edit.Param;
import com.thaiopensource.relaxng.edit.ParentRefPattern;
import com.thaiopensource.relaxng.edit.Pattern;
import com.thaiopensource.relaxng.edit.PatternVisitor;
import com.thaiopensource.relaxng.edit.RefPattern;
import com.thaiopensource.relaxng.edit.SourceLocation;
import com.thaiopensource.relaxng.edit.TextAnnotation;
import com.thaiopensource.relaxng.edit.TextPattern;
import com.thaiopensource.relaxng.edit.UnaryPattern;
import com.thaiopensource.relaxng.edit.ValuePattern;
import com.thaiopensource.relaxng.edit.VoidVisitor;
import com.thaiopensource.relaxng.edit.ZeroOrMorePattern;
import com.thaiopensource.relaxng.output.OutputDirectory;
import com.thaiopensource.relaxng.output.common.ErrorReporter;
import com.thaiopensource.relaxng.parse.SchemaBuilder;
import com.thaiopensource.util.Utf16;
import com.thaiopensource.util.VoidValue;
import com.thaiopensource.xml.out.CharRepertoire;
import com.thaiopensource.xml.util.WellKnownNamespaces;

class Output
{
  private final Prettyprinter pp;
  private final CharRepertoire cr;
  private final String indent;
  private final String sourceUri;
  private final OutputDirectory od;
  private final ErrorReporter er;
  private final NamespaceManager.NamespaceBindings nsb;
  private final Map <String, String> datatypeLibraryMap = new HashMap <String, String> ();
  private final ComplexityCache complexityCache = new ComplexityCache ();
  private final NameClassVisitor <VoidValue> nameClassOutput = new NameClassOutput (true);
  private final NameClassVisitor <VoidValue> noParenNameClassOutput = new NameClassOutput (false);
  private final PatternVisitor <VoidValue> noParenPatternOutput = new PatternOutput (false);
  private final PatternVisitor <VoidValue> patternOutput = new PatternOutput (true);
  private final PatternVisitor <VoidValue> repeatedPatternOutput = new RepeatedPatternOutput ();
  private final ComponentVisitor <VoidValue> componentOutput = new ComponentOutput ();
  private final AnnotationChildVisitor <VoidValue> annotationChildOutput = new AnnotationChildOutput ();
  private final AnnotationChildVisitor <VoidValue> followingAnnotationChildOutput = new FollowingAnnotationChildOutput ();
  private boolean isAttributeNameClass;
  private final StringBuffer encodeBuf = new StringBuffer ();

  static private final String [] keywords = { "attribute",
                                             "default",
                                             "datatypes",
                                             "div",
                                             "element",
                                             "empty",
                                             "external",
                                             "grammar",
                                             "include",
                                             "inherit",
                                             "list",
                                             "mixed",
                                             "namespace",
                                             "notAllowed",
                                             "parent",
                                             "start",
                                             "string",
                                             "text",
                                             "token" };

  static private final Set <String> keywordSet = new HashSet <String> ();

  static
  {
    for (final String keyword : keywords)
      keywordSet.add (keyword);
  }

  static void output (final Pattern p,
                      final String encoding,
                      final String sourceUri,
                      final OutputDirectory od,
                      final ErrorReporter er) throws IOException
  {
    try
    {
      new Output (sourceUri, encoding, od, er, NamespaceVisitor.createBindings (p)).topLevel (p);
    }
    catch (final Prettyprinter.WrappedException e)
    {
      throw e.getIOException ();
    }
  }

  private Output (final String sourceUri,
                  String encoding,
                  final OutputDirectory od,
                  final ErrorReporter er,
                  final NamespaceManager.NamespaceBindings nsb) throws IOException
  {
    this.sourceUri = sourceUri;
    this.od = od;
    this.er = er;
    // Only preserve the input encoding if it's one that can be auto-detected.
    if (encoding != null &&
        !encoding.equalsIgnoreCase ("UTF-8") &&
        !encoding.equalsIgnoreCase ("UTF-16") &&
        !encoding.equalsIgnoreCase ("US-ASCII"))
      encoding = null;
    final OutputDirectory.Stream stream = od.open (sourceUri, encoding);
    this.cr = stream.getCharRepertoire ();
    this.pp = new StreamingPrettyprinter (od.getLineLength (), od.getLineSeparator (), stream.getWriter ());
    this.nsb = nsb;
    final char [] tem = new char [od.getIndent ()];
    for (int i = 0; i < tem.length; i++)
      tem[i] = ' ';
    this.indent = new String (tem);
  }

  private void topLevel (final Pattern p)
  {
    p.accept (new TextAnnotationMerger ());
    final boolean implicitGrammar = p instanceof GrammarPattern && p.getAttributeAnnotations ().isEmpty ();
    if (implicitGrammar && !p.getLeadingComments ().isEmpty ())
    {
      leadingComments (p);
      pp.hardNewline ();
    }
    outputNamespaceDeclarations ();
    outputDatatypeLibraryDeclarations (p);
    if (implicitGrammar)
    {
      for (final AnnotationChild annotationChild : p.getChildElementAnnotations ())
      {
        annotationChild.accept (annotationChildOutput);
        pp.hardNewline ();
      }
      innerBody (((GrammarPattern) p).getComponents ());
      // This deals with trailing comments
      for (final AnnotationChild annotationChild : p.getFollowingElementAnnotations ())
      {
        pp.hardNewline ();
        annotationChild.accept (annotationChildOutput);
      }
    }
    else
      p.accept (patternOutput);
    // The pretty printer ensures that we have a terminating newline.
    pp.close ();
  }

  private void outputNamespaceDeclarations ()
  {
    final List <String> prefixes = new Vector <String> ();
    prefixes.addAll (nsb.getPrefixes ());
    Collections.sort (prefixes);

    boolean needNewline = false;

    String defaultPrefix = null;
    final String defaultNamespace = nsb.getNamespaceUri ("");
    if (defaultNamespace != null && !defaultNamespace.equals (SchemaBuilder.INHERIT_NS))
      defaultPrefix = nsb.getNonEmptyPrefix (defaultNamespace);
    for (final String prefix : prefixes)
    {
      final String ns = nsb.getNamespaceUri (prefix);
      if (prefix.length () == 0)
      {
        if (defaultPrefix == null && !ns.equals (SchemaBuilder.INHERIT_NS))
        {
          pp.startGroup ();
          pp.text ("default namespace =");
          pp.startNest (indent);
          pp.softNewline (" ");
          literal (ns);
          pp.endNest ();
          pp.endGroup ();
          pp.hardNewline ();
          needNewline = true;
        }
      }
      else
        if (!prefix.equals ("xml"))
        {
          pp.startGroup ();
          if (prefix.equals (defaultPrefix))
            pp.text ("default namespace ");
          else
            pp.text ("namespace ");
          encodedText (prefix);
          pp.text (" =");
          pp.startNest (indent);
          pp.softNewline (" ");
          if (ns.equals (SchemaBuilder.INHERIT_NS))
            pp.text ("inherit");
          else
            literal (ns);
          pp.endNest ();
          pp.endGroup ();
          pp.hardNewline ();
          needNewline = true;
        }
    }

    if (needNewline)
      pp.hardNewline ();
  }

  private void outputDatatypeLibraryDeclarations (final Pattern p)
  {
    datatypeLibraryMap.put (WellKnownNamespaces.XML_SCHEMA_DATATYPES, "xsd");
    final List <String> datatypeLibraries = new Vector <String> ();
    datatypeLibraries.addAll (DatatypeLibraryVisitor.findDatatypeLibraries (p));
    if (datatypeLibraries.isEmpty ())
      return;
    Collections.sort (datatypeLibraries);
    for (int i = 0, len = datatypeLibraries.size (); i < len; i++)
    {
      String prefix = "d";
      if (len > 1)
        prefix += Integer.toString (i + 1);
      final String uri = datatypeLibraries.get (i);
      datatypeLibraryMap.put (uri, prefix);
      pp.startGroup ();
      pp.text ("datatypes ");
      encodedText (prefix);
      pp.text (" =");
      pp.startNest (indent);
      pp.softNewline (" ");
      literal (uri);
      pp.endNest ();
      pp.endGroup ();
      pp.hardNewline ();
    }
    pp.hardNewline ();
  }

  private static class TextAnnotationMerger extends VoidVisitor
  {
    @Override
    public void voidVisitElement (final ElementAnnotation ea)
    {
      TextAnnotation prevText = null;
      for (final Iterator <AnnotationChild> iter = ea.getChildren ().iterator (); iter.hasNext ();)
      {
        final AnnotationChild child = iter.next ();
        if (child instanceof TextAnnotation)
        {
          if (prevText == null)
            prevText = (TextAnnotation) child;
          else
          {
            prevText.setValue (prevText.getValue () + ((TextAnnotation) child).getValue ());
            iter.remove ();
          }
        }
        else
        {
          prevText = null;
          child.accept (this);
        }
      }
    }
  }

  static class DatatypeLibraryVisitor extends VoidVisitor
  {
    private final Set <String> datatypeLibraries = new HashSet <String> ();

    @Override
    public void voidVisitValue (final ValuePattern p)
    {
      noteDatatypeLibrary (p.getDatatypeLibrary ());
      super.voidVisitValue (p);
    }

    @Override
    public void voidVisitData (final DataPattern p)
    {
      noteDatatypeLibrary (p.getDatatypeLibrary ());
      super.voidVisitData (p);
    }

    private void noteDatatypeLibrary (final String uri)
    {
      if (!uri.equals ("") && !uri.equals (WellKnownNamespaces.XML_SCHEMA_DATATYPES))
        datatypeLibraries.add (uri);
    }

    static Set <String> findDatatypeLibraries (final Pattern p)
    {
      final DatatypeLibraryVisitor dlv = new DatatypeLibraryVisitor ();
      p.accept (dlv);
      return dlv.datatypeLibraries;
    }
  }

  static class NamespaceVisitor extends VoidVisitor
  {
    private final NamespaceManager nsm = new NamespaceManager ();
    private boolean isAttribute;

    @Override
    public void voidVisitInclude (final IncludeComponent c)
    {
      super.voidVisitInclude (c);
      nsm.requireNamespace (c.getNs (), true);
    }

    @Override
    public void voidVisitExternalRef (final ExternalRefPattern p)
    {
      super.voidVisitExternalRef (p);
      nsm.requireNamespace (p.getNs (), true);
    }

    @Override
    public void voidVisitElement (final ElementPattern p)
    {
      isAttribute = false;
      super.voidVisitElement (p);
    }

    @Override
    public void voidVisitAttribute (final AttributePattern p)
    {
      isAttribute = true;
      super.voidVisitAttribute (p);
    }

    @Override
    public void voidVisitName (final NameNameClass nc)
    {
      super.voidVisitName (nc);
      if (!isAttribute || nc.getNamespaceUri ().length () != 0)
        nsm.requireNamespace (nc.getNamespaceUri (), !isAttribute);
      if (nc.getPrefix () == null)
      {
        if (!isAttribute)
          nsm.preferBinding ("", nc.getNamespaceUri ());
      }
      else
        nsm.preferBinding (nc.getPrefix (), nc.getNamespaceUri ());
    }

    @Override
    public void voidVisitNsName (final NsNameNameClass nc)
    {
      super.voidVisitNsName (nc);
      nsm.requireNamespace (nc.getNs (), false);
    }

    @Override
    public void voidVisitValue (final ValuePattern p)
    {
      super.voidVisitValue (p);
      for (final Map.Entry <String, String> entry : p.getPrefixMap ().entrySet ())
      {
        nsm.requireBinding (entry.getKey (), entry.getValue ());
      }
    }

    @Override
    public void voidVisitElement (final ElementAnnotation ea)
    {
      super.voidVisitElement (ea);
      noteAnnotationBinding (ea.getPrefix (), ea.getNamespaceUri ());
      noteContext (ea.getContext (), true);
    }

    private void noteContext (final NamespaceContext context, final boolean required)
    {
      if (context == null)
        return;
      for (final String prefix : context.getPrefixes ())
      {
        // Default namespace is not relevant to annotations
        if (!prefix.equals (""))
        {
          final String ns = context.getNamespace (prefix);
          if (ns != null && !ns.equals (SchemaBuilder.INHERIT_NS))
          {
            if (required)
              nsm.requireBinding (prefix, ns);
            else
              nsm.preferBinding (prefix, ns);
          }
        }
      }
    }

    @Override
    public void voidVisitAttribute (final AttributeAnnotation a)
    {
      super.voidVisitAttribute (a);
      noteAnnotationBinding (a.getPrefix (), a.getNamespaceUri ());
    }

    private void noteAnnotationBinding (final String prefix, final String ns)
    {
      if (ns.length () != 0)
        nsm.requireNamespace (ns, false);
      if (prefix != null)
        nsm.preferBinding (prefix, ns);
    }

    @Override
    public void voidVisitAnnotated (final Annotated p)
    {
      p.leadingCommentsAccept (this);
      noteContext (p.getContext (), !p.getAttributeAnnotations ().isEmpty ());
      p.attributeAnnotationsAccept (this);
      final List <AnnotationChild> before = (p.mayContainText () ? p.getFollowingElementAnnotations ()
                                                                : p.getChildElementAnnotations ());
      // Avoid unnecessary prefix for documentation
      int state = 0;
      for (final AnnotationChild child : before)
      {
        if (state < 2 && documentationString (child) != null)
          state = 1;
        else
          if (state != 1 || !(child instanceof Comment))
            state = 2;
        if (state == 2)
          child.accept (this);
      }
      if (!p.mayContainText ())
        p.followingElementAnnotationsAccept (this);
    }

    static NamespaceManager.NamespaceBindings createBindings (final Pattern p)
    {
      final NamespaceVisitor nsv = new NamespaceVisitor ();
      p.accept (nsv);
      return nsv.nsm.createBindings ();
    }
  }

  private class ComponentOutput implements ComponentVisitor <VoidValue>
  {
    public VoidValue visitDefine (final DefineComponent c)
    {
      startAnnotations (c);
      pp.startGroup ();
      final String name = c.getName ();
      if (name == DefineComponent.START)
        pp.text ("start");
      else
        identifier (name);
      final Combine combine = c.getCombine ();
      String op;
      if (combine == null)
        op = " =";
      else
        if (combine == Combine.CHOICE)
          op = " |=";
        else
          op = " &=";
      pp.text (op);
      pp.startNest (indent);
      pp.softNewline (" ");
      c.getBody ().accept (noParenPatternOutput);
      pp.endNest ();
      pp.endGroup ();
      endAnnotations (c);
      return VoidValue.VOID;
    }

    public VoidValue visitDiv (final DivComponent c)
    {
      startAnnotations (c);
      pp.text ("div");
      body (c);
      endAnnotations (c);
      return VoidValue.VOID;
    }

    public VoidValue visitInclude (final IncludeComponent c)
    {
      startAnnotations (c);
      pp.startGroup ();
      pp.text ("include ");
      pp.startNest ("include ");
      literal (od.reference (sourceUri, c.getUri ()));
      inherit (c.getNs ());
      pp.endNest ();
      pp.endGroup ();
      final List <Component> components = c.getComponents ();
      if (!components.isEmpty ())
        body (components);
      endAnnotations (c);
      return VoidValue.VOID;
    }
  }

  class RepeatedPatternOutput extends PatternOutput
  {
    RepeatedPatternOutput ()
    {
      super (true);
    }

    @Override
    protected void postfix (final UnaryPattern p, final String op)
    {
      startAnnotations (p);
      pp.text ("(");
      pp.startNest ("(");
      p.getChild ().accept (repeatedPatternOutput);
      pp.endNest ();
      pp.text (op);
      pp.text (")");
      endAnnotations (p);
    }
  }

  class PatternOutput implements PatternVisitor <VoidValue>
  {
    private final boolean alwaysUseParens;

    PatternOutput (final boolean alwaysUseParens)
    {
      this.alwaysUseParens = alwaysUseParens;
    }

    public VoidValue visitGrammar (final GrammarPattern p)
    {
      startAnnotations (p);
      pp.text ("grammar");
      body (p);
      endAnnotations (p);
      return VoidValue.VOID;
    }

    public VoidValue visitElement (final ElementPattern p)
    {
      isAttributeNameClass = false;
      nameClassed (p, "element ");
      return VoidValue.VOID;
    }

    public VoidValue visitAttribute (final AttributePattern p)
    {
      isAttributeNameClass = true;
      nameClassed (p, "attribute ");
      return VoidValue.VOID;
    }

    private void nameClassed (final NameClassedPattern p, final String key)
    {
      startAnnotations (p);
      pp.text (key);
      pp.startNest (key);
      p.getNameClass ().accept (noParenNameClassOutput);
      pp.endNest ();
      braceChild (p);
      endAnnotations (p);
    }

    private void braceChild (final UnaryPattern p)
    {
      final Pattern child = p.getChild ();
      final boolean isSimple = !complexityCache.isComplex (child);
      if (isSimple)
        pp.startGroup ();
      pp.text (" {");
      pp.startNest (indent);
      if (isSimple)
        pp.softNewline (" ");
      else
        pp.hardNewline ();
      child.accept (noParenPatternOutput);
      pp.endNest ();
      if (isSimple)
        pp.softNewline (" ");
      else
        pp.hardNewline ();
      pp.text ("}");
      if (isSimple)
        pp.endGroup ();
    }

    public VoidValue visitOneOrMore (final OneOrMorePattern p)
    {
      postfix (p, "+");
      return VoidValue.VOID;
    }

    public VoidValue visitZeroOrMore (final ZeroOrMorePattern p)
    {
      postfix (p, "*");
      return VoidValue.VOID;
    }

    public VoidValue visitOptional (final OptionalPattern p)
    {
      postfix (p, "?");
      return VoidValue.VOID;
    }

    protected void postfix (final UnaryPattern p, final String op)
    {
      if (!startAnnotations (p))
      {
        p.getChild ().accept (repeatedPatternOutput);
        pp.text (op);
      }
      else
      {
        pp.text ("(");
        pp.startNest ("(");
        p.getChild ().accept (repeatedPatternOutput);
        pp.endNest ();
        pp.text (op);
        pp.text (")");
      }
      endAnnotations (p);
    }

    public VoidValue visitRef (final RefPattern p)
    {
      startAnnotations (p);
      identifier (p.getName ());
      endAnnotations (p);
      return VoidValue.VOID;
    }

    public VoidValue visitParentRef (final ParentRefPattern p)
    {
      startAnnotations (p);
      pp.text ("parent ");
      identifier (p.getName ());
      endAnnotations (p);
      return VoidValue.VOID;
    }

    public VoidValue visitExternalRef (final ExternalRefPattern p)
    {
      startAnnotations (p);
      pp.startGroup ();
      pp.text ("external ");
      pp.startNest ("external ");
      literal (od.reference (sourceUri, p.getUri ()));
      inherit (p.getNs ());
      pp.endNest ();
      pp.endGroup ();
      endAnnotations (p);
      return VoidValue.VOID;
    }

    public VoidValue visitText (final TextPattern p)
    {
      startAnnotations (p);
      pp.text ("text");
      endAnnotations (p);
      return VoidValue.VOID;
    }

    public VoidValue visitEmpty (final EmptyPattern p)
    {
      startAnnotations (p);
      pp.text ("empty");
      endAnnotations (p);
      return VoidValue.VOID;
    }

    public VoidValue visitNotAllowed (final NotAllowedPattern p)
    {
      startAnnotations (p);
      pp.text ("notAllowed");
      endAnnotations (p);
      return VoidValue.VOID;
    }

    public VoidValue visitList (final ListPattern p)
    {
      prefix (p, "list");
      return VoidValue.VOID;
    }

    public VoidValue visitMixed (final MixedPattern p)
    {
      prefix (p, "mixed");
      return VoidValue.VOID;
    }

    private void prefix (final UnaryPattern p, final String key)
    {
      startAnnotations (p);
      pp.text (key);
      braceChild (p);
      endAnnotations (p);
    }

    public VoidValue visitChoice (final ChoicePattern p)
    {
      composite (p, "| ", false);
      return VoidValue.VOID;
    }

    public VoidValue visitInterleave (final InterleavePattern p)
    {
      composite (p, "& ", false);
      return VoidValue.VOID;
    }

    public VoidValue visitGroup (final GroupPattern p)
    {
      composite (p, ",", true);
      return VoidValue.VOID;
    }

    void composite (final CompositePattern p, final String sep, final boolean sepBeforeNewline)
    {
      boolean useParens = alwaysUseParens;
      if (startAnnotations (p))
        useParens = true;
      final boolean isSimple = !complexityCache.isComplex (p);
      if (isSimple)
        pp.startGroup ();
      if (useParens)
      {
        pp.text ("(");
        pp.startNest ("(");
      }

      boolean first = true;
      for (final Pattern child : p.getChildren ())
      {
        if (!first)
        {
          if (sepBeforeNewline)
            pp.text (sep);
          if (isSimple)
            pp.softNewline (" ");
          else
            pp.hardNewline ();
          if (!sepBeforeNewline)
          {
            pp.text (sep);
            pp.startNest (sep);
          }
        }
        child.accept (patternOutput);
        if (first)
          first = false;
        else
          if (!sepBeforeNewline)
            pp.endNest ();
      }
      if (useParens)
      {
        pp.endNest ();
        pp.text (")");
      }
      if (isSimple)
        pp.endGroup ();
      endAnnotations (p);
    }

    public VoidValue visitData (final DataPattern p)
    {
      startAnnotations (p);
      final String lib = p.getDatatypeLibrary ();
      String qn;
      if (!lib.equals (""))
        qn = datatypeLibraryMap.get (lib) + ":" + p.getType ();
      else
        qn = p.getType ();
      qn = encode (qn);
      final Pattern e = p.getExcept ();
      // open a parentheses on data except
      if (e != null)
        pp.text ("(");
      pp.text (qn);
      final List <Param> params = p.getParams ();
      if (params.size () > 0)
      {
        pp.startGroup ();
        pp.text (" {");
        pp.startNest (indent);
        for (final Param param : params)
        {
          pp.softNewline (" ");
          startAnnotations (param);
          pp.startGroup ();
          encodedText (param.getName ());
          pp.text (" =");
          pp.startNest (indent);
          pp.softNewline (" ");
          literal (param.getValue ());
          pp.endNest ();
          pp.endGroup ();
          endAnnotations (param);
        }
        pp.endNest ();
        pp.softNewline (" ");
        pp.text ("}");
        pp.endGroup ();
      }
      if (e != null)
      {
        final boolean useParen = (!e.mayContainText () && !e.getFollowingElementAnnotations ().isEmpty ());
        String s;
        if (params.isEmpty ())
          s = " - ";
        else
        {
          pp.startGroup ();
          pp.softNewline (" ");
          s = "- ";
        }
        if (useParen)
          s += "(";
        pp.text (s);
        pp.startNest (params.isEmpty () ? qn + s : s);
        e.accept (useParen ? noParenPatternOutput : patternOutput);
        pp.endNest ();
        if (useParen)
          pp.text (")");
        if (!params.isEmpty ())
          pp.endGroup ();
        // close the parentheses on data except
        pp.text (")");
      }
      endAnnotations (p);
      return VoidValue.VOID;
    }

    public VoidValue visitValue (final ValuePattern p)
    {
      for (final Map.Entry <String, String> entry : p.getPrefixMap ().entrySet ())
      {
        final String prefix = entry.getKey ();
        final String uri = entry.getValue ();
        if (!uri.equals (nsb.getNamespaceUri (prefix)))
        {
          if (prefix.equals (""))
            er.error ("value_inconsistent_default_binding", uri, p.getSourceLocation ());
          else
            er.error ("value_inconsistent_binding", prefix, uri, p.getSourceLocation ());
        }
      }
      startAnnotations (p);
      final String lib = p.getDatatypeLibrary ();
      pp.startGroup ();
      String str = null;
      if (lib.equals (""))
      {
        if (!p.getType ().equals ("token"))
          str = p.getType () + " ";
      }
      else
        str = datatypeLibraryMap.get (lib) + ":" + p.getType () + " ";
      if (str != null)
      {
        final String encoded = encode (str);
        pp.text (encoded);
        pp.startNest (encoded);
      }
      literal (p.getValue ());
      if (str != null)
        pp.endNest ();
      pp.endGroup ();
      endAnnotations (p);
      return VoidValue.VOID;
    }

  }

  class NameClassOutput implements NameClassVisitor <VoidValue>
  {
    private final boolean alwaysUseParens;

    NameClassOutput (final boolean alwaysUseParens)
    {
      this.alwaysUseParens = alwaysUseParens;
    }

    public VoidValue visitAnyName (final AnyNameNameClass nc)
    {
      final NameClass e = nc.getExcept ();
      if (e == null)
      {
        startAnnotations (nc);
        pp.text ("*");
      }
      else
      {
        final boolean useParens = startAnnotations (nc) || alwaysUseParens;
        final String s = useParens ? "(* - " : "* - ";
        pp.text (s);
        pp.startNest (s);
        e.accept (nameClassOutput);
        if (useParens)
          pp.text (")");
        pp.endNest ();
      }
      endAnnotations (nc);
      return VoidValue.VOID;
    }

    public VoidValue visitNsName (final NsNameNameClass nc)
    {
      final NameClass e = nc.getExcept ();
      final String prefix = nsb.getNonEmptyPrefix (nc.getNs ());
      if (e == null)
      {
        startAnnotations (nc);
        encodedText (prefix);
        pp.text (":*");
      }
      else
      {
        final boolean useParens = startAnnotations (nc) || alwaysUseParens;
        String s = useParens ? "(" : "";
        s += encode (prefix);
        s += ":* - ";
        pp.text (s);
        pp.startNest (s);
        e.accept (nameClassOutput);
        pp.endNest ();
        if (useParens)
          pp.text (")");
      }
      endAnnotations (nc);
      return VoidValue.VOID;
    }

    public VoidValue visitName (final NameNameClass nc)
    {
      startAnnotations (nc);
      qualifiedName (nc.getNamespaceUri (), nc.getPrefix (), nc.getLocalName (), isAttributeNameClass);
      endAnnotations (nc);
      return VoidValue.VOID;
    }

    public VoidValue visitChoice (final ChoiceNameClass nc)
    {
      boolean useParens = alwaysUseParens;
      if (startAnnotations (nc))
        useParens = true;
      else
        if (nc.getChildren ().size () == 1)
          useParens = false;
      if (useParens)
      {
        pp.text ("(");
        pp.startNest ("(");
      }
      pp.startGroup ();
      boolean first = true;
      for (final NameClass child : nc.getChildren ())
      {
        if (first)
          first = false;
        else
        {
          pp.softNewline (" ");
          pp.text ("| ");
        }
        child.accept (nameClassOutput);
      }
      pp.endGroup ();
      if (useParens)
      {
        pp.endNest ();
        pp.text (")");
      }
      endAnnotations (nc);
      return VoidValue.VOID;
    }
  }

  class AnnotationChildOutput implements AnnotationChildVisitor <VoidValue>
  {
    public VoidValue visitText (final TextAnnotation ta)
    {
      literal (ta.getValue ());
      return VoidValue.VOID;
    }

    public VoidValue visitComment (final Comment c)
    {
      comment ("#", c.getValue ());
      return VoidValue.VOID;
    }

    public VoidValue visitElement (final ElementAnnotation elem)
    {
      checkContext (elem.getContext (), elem.getSourceLocation ());
      qualifiedName (elem.getNamespaceUri (), elem.getPrefix (), elem.getLocalName (),
      // unqualified annotation element names have "" namespace
                     true);
      pp.text (" ");
      annotationBody (elem.getAttributes (), elem.getChildren ());
      return VoidValue.VOID;
    }
  }

  class FollowingAnnotationChildOutput extends AnnotationChildOutput
  {
    @Override
    public VoidValue visitElement (final ElementAnnotation elem)
    {
      pp.text (">> ");
      pp.startNest (">> ");
      super.visitElement (elem);
      pp.endNest ();
      return VoidValue.VOID;
    }
  }

  private static boolean hasAnnotations (final Annotated annotated)
  {
    return (!annotated.getChildElementAnnotations ().isEmpty () || !annotated.getAttributeAnnotations ().isEmpty () || !annotated.getFollowingElementAnnotations ()
                                                                                                                                 .isEmpty ());
  }

  private boolean startAnnotations (final Annotated annotated)
  {
    if (!annotated.getLeadingComments ().isEmpty ())
    {
      leadingComments (annotated);
      if (!hasAnnotations (annotated))
        return false;
    }
    else
      if (!hasAnnotations (annotated))
        return false;
    List <AnnotationChild> before = (annotated.mayContainText () ? annotated.getFollowingElementAnnotations ()
                                                                : annotated.getChildElementAnnotations ());
    int i = 0;
    final int len = before.size ();
    for (; i < len; i++)
    {
      int j = i;
      if (i != 0)
      {
        do
        {
          if (!(before.get (j) instanceof Comment))
            break;
        } while (++j < len);
        if (j >= len)
          break;
      }
      final String doc = documentationString (before.get (j));
      if (doc == null)
        break;
      if (j == i)
        pp.hardNewline ();
      else
      {
        for (;;)
        {
          before.get (i).accept (annotationChildOutput);
          if (++i == j)
            break;
          pp.hardNewline ();
        }
      }
      comment ("##", doc);
    }
    if (i > 0)
      before = before.subList (i, len);
    pp.startGroup ();
    if (!annotated.getAttributeAnnotations ().isEmpty () || !before.isEmpty ())
    {
      if (!annotated.getAttributeAnnotations ().isEmpty ())
        checkContext (annotated.getContext (), annotated.getSourceLocation ());
      annotationBody (annotated.getAttributeAnnotations (), before);
      pp.softNewline (" ");
    }
    return true;
  }

  private static String documentationString (final AnnotationChild child)
  {
    if (!(child instanceof ElementAnnotation))
      return null;
    final ElementAnnotation elem = (ElementAnnotation) child;
    if (!elem.getLocalName ().equals ("documentation"))
      return null;
    if (!elem.getNamespaceUri ().equals (WellKnownNamespaces.RELAX_NG_COMPATIBILITY_ANNOTATIONS))
      return null;
    if (!elem.getAttributes ().isEmpty ())
      return null;
    final StringBuffer buf = new StringBuffer ();
    for (final AnnotationChild a : elem.getChildren ())
    {
      if (!(a instanceof TextAnnotation))
        return null;
      buf.append (((TextAnnotation) a).getValue ());
    }
    return buf.toString ();
  }

  private void endAnnotations (final Annotated annotated)
  {
    if (!annotated.mayContainText ())
    {
      for (final AnnotationChild child : annotated.getFollowingElementAnnotations ())
      {
        if (annotated instanceof Component)
          pp.hardNewline ();
        else
          pp.softNewline (" ");
        final AnnotationChildVisitor <VoidValue> output = (annotated instanceof Component
                                                                                         ? annotationChildOutput
                                                                                         : followingAnnotationChildOutput);
        child.accept (output);
      }
    }
    if (hasAnnotations (annotated))
      pp.endGroup ();
  }

  private void leadingComments (final Annotated annotated)
  {
    boolean first = true;
    for (final Comment comment : annotated.getLeadingComments ())
    {
      if (!first)
        pp.hardNewline ();
      else
        first = false;
      comment.accept (annotationChildOutput);
    }
  }

  private void annotationBody (final List <AttributeAnnotation> attributes, final List <AnnotationChild> children)
  {
    pp.startGroup ();
    pp.text ("[");
    pp.startNest (indent);
    for (final AttributeAnnotation att : attributes)
    {
      pp.softNewline (" ");
      pp.startGroup ();
      qualifiedName (att.getNamespaceUri (), att.getPrefix (), att.getLocalName (), true);
      pp.text (" =");
      pp.startNest (indent);
      pp.softNewline (" ");
      literal (att.getValue ());
      pp.endNest ();
      pp.endGroup ();
    }
    for (final AnnotationChild child : children)
    {
      pp.softNewline (" ");
      child.accept (annotationChildOutput);
    }
    pp.endNest ();
    pp.softNewline (" ");
    pp.text ("]");
    pp.endGroup ();
  }

  private void body (final Container container)
  {
    body (container.getComponents ());
  }

  private void body (final List <Component> components)
  {
    if (components.size () == 0)
      pp.text (" { }");
    else
    {
      pp.text (" {");
      pp.startNest (indent);
      pp.hardNewline ();
      innerBody (components);
      pp.endNest ();
      pp.hardNewline ();
      pp.text ("}");
    }
  }

  private void innerBody (final List <Component> components)
  {
    boolean first = true;
    for (final Component c : components)
    {
      if (first)
        first = false;
      else
        pp.hardNewline ();
      c.accept (componentOutput);
    }
  }

  private void inherit (final String ns)
  {
    if (ns.equals (nsb.getNamespaceUri ("")))
      return;
    pp.softNewline (" ");
    pp.text ("inherit = ");
    encodedText (nsb.getNonEmptyPrefix (ns));
  }

  private void identifier (final String name)
  {
    if (keywordSet.contains (name))
      pp.text ("\\");
    encodedText (name);
  }

  private static final String [] delims = { "\"", "'", "\"\"\"", "'''" };

  private void literal (final String str)
  {
    for (int i = 0, len = str.length ();;)
    {
      // Find the delimiter that gives the longest segment
      String bestDelim = null;
      int bestEnd = -1;
      int lim = str.indexOf ('\n', i);
      if (lim < 0)
        lim = len;
      else
        ++lim;
      for (final String delim : delims)
      {
        final int end = (str + delim).indexOf (delim, i);
        if (end > bestEnd)
        {
          bestDelim = delim;
          bestEnd = end;
          if (end >= lim)
          {
            bestEnd = lim;
            break;
          }
        }
      }
      if (i != 0)
      {
        pp.text (" ~");
        pp.softNewline (" ");
      }
      pp.text (bestDelim);
      encodedText (str.substring (i, bestEnd));
      pp.text (bestDelim);
      i = bestEnd;
      if (i == len)
        break;
    }
  }

  private void encodedText (final String str)
  {
    pp.text (encode (str));
  }

  private String encode (String str)
  {
    int start = 0;
    final int len = str.length ();
    for (int i = 0; i < len; i++)
    {
      final char c = str.charAt (i);
      switch (c)
      {
        case '\\':
          if (!startsWithEscapeOpen (str, i))
            break;
          // fall through
        case '\r':
        case '\n':
          if (start < i)
            encodeBuf.append (str.substring (start, i));
          escape (c);
          start = i + 1;
          break;
        default:
          if (Utf16.isSurrogate (c))
          {
            if (!cr.contains (c, str.charAt (i + 1)))
            {
              if (start < i)
                encodeBuf.append (str.substring (start, i));
              escape (Utf16.scalarValue (c, str.charAt (i + 1)));
              start = i + 2;
            }
            ++i;
          }
          else
            if (!cr.contains (c))
            {
              if (start < i)
                encodeBuf.append (str.substring (start, i));
              escape (c);
              start = i + 1;
            }
          break;
      }
    }
    if (start == 0)
      return str;
    if (start != len)
      encodeBuf.append (str.substring (start, len));
    str = encodeBuf.toString ();
    encodeBuf.setLength (0);
    return str;
  }

  private void escape (final int n)
  {
    encodeBuf.append ("\\x{");
    encodeBuf.append (Integer.toHexString (n));
    encodeBuf.append ("}");
  }

  static private boolean startsWithEscapeOpen (final String str, int off)
  {
    if (!str.startsWith ("\\x", off))
      return false;
    for (off += 2; str.startsWith ("x", off); off++)
    {}
    return str.startsWith ("{", off);
  }

  /**
   * null means no prefix
   */
  private void qualifiedName (final String ns, String prefix, final String localName, final boolean isAttribute)
  {
    prefix = choosePrefix (ns, prefix, isAttribute);
    if (prefix == null)
      encodedText (localName);
    else
    {
      encodedText (prefix);
      pp.text (":");
      encodedText (localName);
    }
  }

  /**
   * null means no prefix
   */
  private String choosePrefix (final String ns, final String prefix, final boolean isAttribute)
  {
    if (prefix != null && ns.equals (nsb.getNamespaceUri (prefix)))
      return prefix;
    if (isAttribute)
    {
      if (ns.length () == 0)
        return null;
    }
    else
    {
      if (ns.equals (nsb.getNamespaceUri ("")))
        return null;
    }
    return nsb.getNonEmptyPrefix (ns);
  }

  private void comment (final String delim, final String value)
  {
    int i = 0;
    for (;;)
    {
      pp.text (delim);
      if (i < value.length () && value.charAt (i) != '\t')
        pp.text (" ");
      final int j = value.indexOf ('\n', i);
      final String tem = j < 0 ? value.substring (i) : value.substring (i, j);
      encodedText (tem);
      pp.hardNewline ();
      if (j < 0)
        break;
      i = j + 1;
    }
  }

  private void checkContext (final NamespaceContext context, final SourceLocation loc)
  {
    if (context == null)
      return;
    for (final String prefix : context.getPrefixes ())
    {
      // Default namespace is not relevant to annotations
      if (!prefix.equals (""))
      {
        final String ns = context.getNamespace (prefix);
        if (ns != null && !ns.equals (SchemaBuilder.INHERIT_NS) && !nsb.getNamespaceUri (prefix).equals (ns))
          er.warning ("annotation_inconsistent_binding", prefix, ns, loc);
      }
    }
  }
}
